﻿using System;
using System.Diagnostics;
using System.Reflection;
using System.Collections.Generic;
using PostSharp.Sdk.CodeModel;
using PostSharp.Sdk.CodeWeaver;
using PostSharp.Sdk.Collections;
using Young3.FMSearch.Core.Attributes;
using Young3.FMSearch.Core.Managers;
using Young3.FMSearch.Core.Entities.InGame;

namespace Young3.FMSearch.Core
{
    internal class FMEntityWeaveAdvice : IAdvice
    {
        private readonly FMEntityWeaveTask parent;
        private readonly FMEntityAttribute attribute;
        private FieldDefDeclaration bytesFieldDef;
        private FieldDefDeclaration databaseModeFieldDef;
        private FieldDefDeclaration memAddressFieldDef;

        public FMEntityWeaveAdvice(FMEntityWeaveTask parent, FMEntityAttribute attribute)
        {
            this.parent = parent;
            this.attribute = attribute;
        }

        public int Priority
        {
            get { return 1; }
        }

        public bool RequiresWeave(WeavingContext context)
        {
            return true;
        }

        public void Weave(WeavingContext context, InstructionBlock block)
        {
            //player points to baseobject so it's fine
            bytesFieldDef = parent.Project.Module.FindField(
                typeof(Player)
                .GetField("OriginalBytes"),
                BindingOptions.Default).GetFieldDefinition();

            memAddressFieldDef = parent.Project.Module.FindField(
                typeof(Player)
                .GetField("MemoryAddress"),
                BindingOptions.Default).GetFieldDefinition();

            databaseModeFieldDef = parent.Project.Module.FindField(
                typeof(Player)
                .GetField("DatabaseMode"),
                BindingOptions.Default).GetFieldDefinition();

            if (context.Method.ReturnParameter.ParameterType.GetSystemType(null, null) == typeof(string))
            {
                this.WeaveGetterString(context, block);
            }
            else if (context.Method.ReturnParameter.ParameterType.GetSystemType(null, null) != typeof(void))
            {
                this.WeaveGetter(context, block);
            }
            else
            {
                //setter, not implemented yet
                this.WeaveSetter(context, block);
            }
        }

        private void WeaveSetter(WeavingContext context, InstructionBlock block)
        {
            //public static void Set<T>(int offset, T newValue)
            InstructionSequence innerBodySequence = context.Method.MethodBody.CreateInstructionSequence();
            block.AddInstructionSequence(innerBodySequence, NodePosition.Before, null);
            context.InstructionWriter.AttachInstructionSequence(innerBodySequence);

            MethodBase m = typeof(PropertyInvoker).GetMethod("Set").MakeGenericMethod(context.Method.Parameters[0].ParameterType.GetSystemType(null, null));
            IMethod methodDef2 = parent.Project.Module.FindMethod(m, BindingOptions.RequireGenericInstance);

            context.InstructionWriter.EmitInstructionInt32(OpCodeNumber.Ldc_I4, this.attribute.Offset);

            context.InstructionWriter.EmitInstruction(OpCodeNumber.Ldarg_0);
            context.InstructionWriter.EmitInstructionField(OpCodeNumber.Ldfld, memAddressFieldDef);

            //object value
            context.InstructionWriter.EmitInstruction(OpCodeNumber.Ldarg_1);

            context.InstructionWriter.EmitInstruction(OpCodeNumber.Ldarg_0);
            context.InstructionWriter.EmitInstructionField(OpCodeNumber.Ldfld, databaseModeFieldDef);

            context.InstructionWriter.EmitInstructionMethod(OpCodeNumber.Call, methodDef2);


            context.InstructionWriter.EmitInstruction(OpCodeNumber.Ret);

            context.InstructionWriter.DetachInstructionSequence();
        }

        private void WeaveGetter(WeavingContext context, InstructionBlock block)
        {
            InstructionSequence innerBodySequence = context.Method.MethodBody.CreateInstructionSequence();
            block.AddInstructionSequence(innerBodySequence, NodePosition.Before, null);
            context.InstructionWriter.AttachInstructionSequence(innerBodySequence);

            MethodBase m = typeof(PropertyInvoker).GetMethod("Get").MakeGenericMethod(context.Method.ReturnParameter.ParameterType.GetSystemType(null, null));
            IMethod methodDef2 = parent.Project.Module.FindMethod(m, BindingOptions.RequireGenericInstance);

            context.InstructionWriter.EmitInstructionInt32(OpCodeNumber.Ldc_I4, this.attribute.Offset);

            context.InstructionWriter.EmitInstruction(OpCodeNumber.Ldarg_0);
            context.InstructionWriter.EmitInstructionField(OpCodeNumber.Ldfld, bytesFieldDef);

            context.InstructionWriter.EmitInstruction(OpCodeNumber.Ldarg_0);
            context.InstructionWriter.EmitInstructionField(OpCodeNumber.Ldfld, memAddressFieldDef);

            context.InstructionWriter.EmitInstruction(OpCodeNumber.Ldarg_0);
            context.InstructionWriter.EmitInstructionField(OpCodeNumber.Ldfld, databaseModeFieldDef);

            context.InstructionWriter.EmitInstructionMethod(OpCodeNumber.Call, methodDef2);

            context.InstructionWriter.EmitInstruction(OpCodeNumber.Ret);

            context.InstructionWriter.DetachInstructionSequence();
        }

        private void WeaveGetterString(WeavingContext context, InstructionBlock block)
        {
            InstructionSequence innerBodySequence = context.Method.MethodBody.CreateInstructionSequence();
            block.AddInstructionSequence(innerBodySequence, NodePosition.Before, null);
            context.InstructionWriter.AttachInstructionSequence(innerBodySequence);

            MethodBase m = typeof(PropertyInvoker).GetMethod("GetString");
            IMethod methodDef2 = parent.Project.Module.FindMethod(m, BindingOptions.Default);

            context.InstructionWriter.EmitInstructionInt32(OpCodeNumber.Ldc_I4, this.attribute.Offset);
            context.InstructionWriter.EmitInstructionInt32(OpCodeNumber.Ldc_I4, this.attribute.AdditionalStringOffset);

            context.InstructionWriter.EmitInstruction(OpCodeNumber.Ldarg_0);
            context.InstructionWriter.EmitInstructionField(OpCodeNumber.Ldfld, bytesFieldDef);

            context.InstructionWriter.EmitInstruction(OpCodeNumber.Ldarg_0);
            context.InstructionWriter.EmitInstructionField(OpCodeNumber.Ldfld, memAddressFieldDef);

            context.InstructionWriter.EmitInstruction(OpCodeNumber.Ldarg_0);
            context.InstructionWriter.EmitInstructionField(OpCodeNumber.Ldfld, databaseModeFieldDef);

            context.InstructionWriter.EmitInstructionMethod(OpCodeNumber.Call, methodDef2);

            context.InstructionWriter.EmitInstruction(OpCodeNumber.Ret);

            context.InstructionWriter.DetachInstructionSequence();
        }
    }
}